#-*- coding = utf-8 -*-
#@Time : 2021/8/4 17:52
#@Author : BAYERN
#@File : Bilibili-Video.py
#@software: PyCharm

import requests
from bs4 import BeautifulSoup
import datetime
import time
import tkinter
from tkinter import font
import os
from tkinter import *
from tkinter import filedialog
import urllib.request
import re
import webbrowser
import threading
import json
import Acfun_Video
import DingDianXiaoShuo
import QuanXiaoShuo
import BiQuGe


help_text = '''待优化：
1、GUI界面——框架已有
2、视频无法下载，检查文件内是否有同名文件
3、视频为多P视频的优化，如增加该P小标题，自动全部下载多P
4、番剧下载——支持B站无会员
5、当文件名为罕见字时无法写入—— 解决方案：在title中去除
6、如果下载文件无法打开，尝试将文件后缀改为flv
7、下载进度条刷新太快

支持网站：
acfun       https://www.acfun.cn/
bilibili    https://www.bilibili.com/
顶点小说     https://www.ddxs.cc/
全小说       https://quanxiaoshuo.com/
笔趣阁       https://www.xbiquge.la/
'''

# GUI类
class GUI(object):
    def __init__(self):
        top = tkinter.Tk()
        top.title("视频下载器")
        top.geometry('1000x800')
        self.url = ''

        def do_button_1():
            self.url = self.entry_1.get()   # 获取用户输入网址
            self.text.delete(1.0, END) # 删除文本框内容

            '''判断网址是哪一个网址'''
            # A站
            # B站
            # 顶点小说
            # 全小说
            # 笔趣阁
            url_type = url_check(self.url)

            '''根据网址调用不同函数'''
            if url_type == 'acfun':
                Acfun_Video.main(self.url,self)
            # B站网址
            elif url_type == 'bilibili':
                '''判断网址是否符合规范--待优化'''
                bilibili_url = re.compile('(?:https?://)?www.bilibili.com/video/.+')
                match_bangumi = re.compile('(?:https?://)?www.bilibili.com/bangumi/play/[sep]+([A-Za-z0-9]+)')

                # 如果是video网页
                if re.match(bilibili_url, self.url) != None:
                    Bilibili_video(self.url,gui=self)

                # 如果是番剧
                elif re.match(match_bangumi, self.url) != None:
                    API_download(self.url,gui=self)
            # 顶点小说
            elif url_type == 'ddxs':
                DingDianXiaoShuo.down_ddxs(self.url,self)
            # 全小说
            elif url_type == 'quanxiaoshuo':
                QuanXiaoShuo.main(self.url,gui=self)
            # 笔趣阁
            elif url_type == 'biquge':
                BiQuGe.Biquge(url=self.url,gui=self)
            else:
                self.text.insert(END, "输入的网址不合法或无法下载\n")
                print('输入的网址不合法')
                return

        def do_button_3():
            self.text.insert(END,help_text)
            self.text.see(END)



        self.label_1 = tkinter.Label(top, text='请在下方输入视频所在的网址')
        self.button_3 = tkinter.Button(top,text='点此查看帮助',command=lambda :thread_it(do_button_3))
        self.button_4 = tkinter.Button(top,text='清空',bd=5,command=lambda :self.entry_1.delete(0,END))
        self.entry_1 = tkinter.Entry(top,width=90)
        self.button_1 = tkinter.Button(top, text='开始下载', command=lambda :thread_it(do_button_1))
        self.label_2 = tkinter.Label(top,text="文本框")
        self.ft = tkinter.font.Font()    # text字体样式
        self.text = tkinter.Text(top,width=90,height=20,font=self.ft)
        self.var = tkinter.StringVar()
        self.label_3 = tkinter.Label(top,textvariable=self.var)
        self.button_2 = tkinter.Button(top,text='退出',command=lambda :sys.exit())

        self.label_1.grid(row=1,column=0)
        self.button_3.grid(row=1,column=0,stick=E,padx=50)
        self.button_4.grid(row=2,column=0,stick=E,padx=1)
        self.entry_1.grid(row=2,padx=10)
        self.button_1.grid(pady=20)
        self.label_2.grid()
        self.text.grid()
        self.label_3.grid(row=6)
        self.button_2.grid(pady=20)

        top.mainloop()

# 多线程
def thread_it(func, *args):
    '''将函数打包进线程'''
    # 创建
    t = threading.Thread(target=func, args=args)
    t.setDaemon(True)
    t.start()


'''函数：url_check
    作用：初步判断是哪一个网址的网站
    参数： 传入：url  返回：str'''
def url_check(url):
    if re.search('acfun',url):
        print("这是acfun网站")
        return 'acfun'
    elif re.search('bilibili',url):
        print("这是bilibili网站")
        return 'bilibili'
    elif re.search('www.ddxs.cc/ddxs/\d+/',url):
        print("这是顶点小说网站")
        return 'ddxs'
    elif re.search('(quanxiaoshuo)|(quanxs.net)',url):
        print("这是全小说网站")
        return 'quanxiaoshuo'
    elif re.search('biquge', url):
        print("这是笔趣阁网站")
        return 'biquge'
    else:
        print("未能检测出是什么网站")
        return None


'''函数：byte_convert
    作用：将字节数转换为合适的单位，以表示文件大小
    参数： 传入：1个str/int/float  返回：str'''
def byte_convert(byte):
    unit = ''
    byte = float(byte)
    '''b转为kb'''
    if byte > 1024:
        unit = 'KB'
        byte = byte/1024
    '''kb转为mb'''
    if byte > 1024:
        unit = 'MB'
        byte = byte/1024
    '''mb转为gb'''
    if byte > 1024:
        unit = 'GB'
        byte = byte/1024
    return str('%.2f' %byte)+unit


'''函数：GetHtml
    作用：得到源网页的HTML，以字符形式返回
    参数： 传入：网页   返回：(str)response.text'''
def GetHtml(url):
    # 表头
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'}
    # 爬热搜网址，返回对象

    try:
        response = requests.get(url=url, headers=headers)
        return response.text

    except:
        print("连接网络失败")
        sys.exit()

def DownHtml(url,name='B站Html',type_='txt'):
    # 表头
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
               'Referer':url,
               'Origin':'https://www.bilibili.com'
               }
    # 爬热搜网址，返回对象
    response = requests.get(url=url, headers=headers)

    with open(str(name)+'.'+str(type_),'wb') as f:
        f.write(response.content)


'''三个参数：视频真实地址，原网页地址（用于referer），本地文件保存路径，gui表示GUI对象
    额外函数：byte_convert'''
def downVideo(video_url,url,path,gui=None):
    # 表头
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
               'Referer':url,
               'Origin':'https://www.bilibili.com'
               }

    print("正在下载")
    if gui != None:  gui.text.insert(END, path+"正在下载\n")

    '''发送请求'''
    try:
        response = requests.get(url=video_url, headers=headers,stream=True)
    except:
        if gui != None:  gui.text.insert(END, "下载文件失败，请重试\n")
        return


    '''得到文件大小'''
    content_length = int(response.headers['content-length'])    # 文件总字节
    chunk_size = 1024    # 请求字节
    data_count = 0  # 已下载的字节数

    print("文件大小为："+byte_convert(response.headers['content-length'])+"\n正在写入本地文件")
    if gui != None:  gui.text.insert(END, "文件大小为："+byte_convert(response.headers['content-length'])+"\n正在写入本地文件\n")

    '''下载文件'''
    with open(path,'wb') as f:
        for data in response.iter_content(chunk_size=chunk_size):
            f.write(data)
            data_count = data_count + len(data)
            percent = int(data_count / content_length * 100)  # 计算下载的进度

            '''在GUI中的中输出'''
            if gui != None :
                gui.var.set("文件下载进度：{}% {}/{}".format(percent, byte_convert(data_count), byte_convert(content_length)))

                # 用文本框输出
                # if gui.text.search('文件下载进度：', '1.0'):
                #     gui.text.delete(gui.text.search('文件下载进度：', '1.0'), END)
                # gui.text.insert(END, "\n文件下载进度：%d%%(%d/%d) - %s" % (percent, data_count, content_length, path))
            print("\r 文件下载进度：{}% {}/{} - {}".format(percent, byte_convert(data_count), byte_convert(content_length),path), end=" ")


'''下载B站视频主函数——通过HTML'''
def Bilibili_video(url,gui=None):
    pass
    '''下载HTML到本地文件'''
    DownHtml(url)
    '''从本地文件中读取HTML'''
    with open('B站Html.txt', 'r', encoding='utf-8') as f:
        text = f.read()

    '''找到标题'''
    findTitle = re.compile('<div id="viewbox_report".*?<span class="tit.*?">(.*?)</span>')
    title = re.findall(findTitle, text)[0]
    # BUG 修复——避免出现某些特殊字符
    findSpecial = re.compile(r'[/\\:*"\'?<>]')
    if len(re.findall(findSpecial,title)) != 0:
        title = re.sub(findSpecial, ' ', title)
        if gui != None:  gui.text.insert(END,'标题中含特殊字符，已用空格替换')
    # 输出标题
    if gui != None:  gui.text.insert(END,title+'\n')
    print(title)

    '''检查是否为多P视频'''
    findMulti_page = re.compile('id="multi_page"(.*?>)')
    if re.search(findMulti_page, text) != None:
        if gui != None:  gui.text.insert(END, "注意：该网页存在多P视频，请分别下载\n注意输入网页格式后带‘p=数字’字样\n")
        print("注意：该网页存在多P视频，请分别下载\n注意输入网页格式后带‘p=数字’字样")

    '''找到视频链接，下载'''
    findVideo = re.compile('"video":\[{"id":\d+,"baseUrl":"(.*?)"')
    # 判断正则表达式是否找到链接
    if re.findall(findVideo,text) == []:  # 如果没有找到链接
        if gui != None:  gui.text.insert(END,'未能找到视频链接\n')
        print("未能找到视频链接")
        sys.exit()
    video_url = re.findall(findVideo,text)[0]
    # 判断当前目录下是否含有video文件夹
    if not os.path.exists(os.getcwd() + '\\video'):
        os.mkdir('video')
    # 判断下载目录中是否存在视频
    if os.path.exists(f'video\{title}.mp4'):
        print("目录中已经存在即将下载的视频")
        if gui != None:  gui.text.insert(END, "目录中已经存在即将下载的视频\n")
        return
    video_path = 'video\\'+title+'-视频文件.mp4'
    print(video_url)
    downVideo(video_url,url,video_path,gui=gui)

    '''找到音频链接，下载'''
    findAudio = re.compile('"audio":\[{"id":\d+,"baseUrl":"(.*?)"')
    # 判断正则表达式是否找到链接
    if re.findall(findAudio, text) == []:  # 如果没有找到链接
        if gui != None:  gui.text.insert(END, '未能找到视频链接\n')
        print("未能找到视频链接")
        sys.exit()
    audio_url = re.findall(findAudio,text)[0]
    print(audio_url)
    audio_path = f'video\{title}-音频文件.mp3'
    downVideo(audio_url,url,audio_path,gui=gui)
    '''合并视频和音频'''
    mix = f'ffmpeg -i "{video_path}" -i "{audio_path}" -c copy "video\{title}.flv"'
    os.system(mix)
    # 删除单独的音频和视频文件
    # os.remove(video_path)
    # os.remove(audio_path)
    if gui != None:  gui.text.insert(END,"视频下载完成\n")
    if gui != None:  gui.text.insert(END, "视频位于"+os.getcwd()+"\\video\n")
    print("视频下载完成")

'''下载番剧——通过API'''
def API_download(url,gui=None):
    '''构造函数'''

    # 正则表达式匹配
    match_BV = re.compile('BV|bv.*')
    match_video = re.compile('(?:https?://)?www.bilibili.com/video/(BV[A-Za-z0-9]+)')
    match_bangumi_ss = re.compile('(?:https?://)?www.bilibili.com/bangumi/play/ss([A-Za-z0-9]+)')
    match_bangumi_ep = re.compile('(?:https?://)?www.bilibili.com/bangumi/play/ep([A-Za-z0-9]+)')


    '''检测url是否合法'''
    # 如果传入的是BV号
    if re.match(match_BV, url) != None:
        print("这是BV", url)

    # 传入的是含BV号的网页
    elif re.match(match_video, url) != None:
        print("这是B站video网页")

    # 传入的是番剧网页(season_id)
    elif re.match(match_bangumi_ss, url) != None:
        print("这是番剧网页(ss)")

        '''得到API数据'''
        api_head = 'https://api.bilibili.com/pgc/web/season/section'
        season_id = re.match(match_bangumi_ss, url).group(1)
        api_url = api_head + '?season_id=' + season_id
        print(api_url)
        text = GetHtml(api_url)

        '''找到avid和cvid'''
        text = json.loads(text)
        # 得到番剧信息列表——格式为list，每一个列表存放一个字典
        bangumiList = text['result']['main_section']['episodes']

        ''''''
        link_head = 'https://api.bilibili.com/x/player/playurl'
        avid = bangumiList[0]['aid']
        cid = bangumiList[0]['cid']

        # 得到含有下载信息的API网址
        api_url = link_head + '?avid=' + str(avid) + '&cid=' + str(cid)+'&qn=64&&fourk=1'

        text = GetHtml(api_url)
        text = json.loads(text)

        '''得到flv下载链接'''
        # 如果不返回data
        if text.get('data') == None:
            if gui != None:  gui.text.insert(END, '无法下载此视频\n')
            print("无法下载此视频")
            return
        video_url = text['data']['durl'][0]['url']
        print(video_url)

        # 判断当前目录下是否含有video文件夹
        if not os.path.exists(os.getcwd() + '\\video'):
            os.mkdir('video')

        video_path = os.getcwd()+'\\video\\番剧.flv'
        downVideo(video_url,url,video_path,gui=gui)
        print("视频下载完毕")
        print("下载视频位于"+video_path)
        if gui != None:  gui.text.insert(END, "视频下载完成\n")
        if gui != None:  gui.text.insert(END, "视频位于" + os.getcwd() + "\\video\n")

    elif re.match(match_bangumi_ep, url) != None:
        print("这是番剧网页(ep)")

        '''得到API数据'''
        api_head = 'https://api.bilibili.com/pgc/view/web/season'
        ep_id = re.match(match_bangumi_ep, url).group(1)
        api_url = api_head + '?ep_id=' + ep_id
        print(api_url)
        text = GetHtml(api_url)

        '''找到番剧信息'''
        text = json.loads(text)     # 转换为json格式
        bangumiList = text['result']['episodes']    # 存储所有番剧信息的列表

        '''找到具体哪一集'''
        for bangumi in bangumiList:
            if bangumi['id'] == int(ep_id):
                avid = bangumi['aid']
                cid = bangumi['cid']
                long_title = bangumi['long_title']
                break

        # 得到含有下载信息的API网址
        link_head = 'https://api.bilibili.com/x/player/playurl'
        api_url = link_head + '?avid=' + str(avid) + '&cid=' + str(cid)+'&qn=64&&fourk=1'

        text = GetHtml(api_url)
        text = json.loads(text)

        '''得到flv下载链接'''
        # 如果不返回data
        if text.get('data') == None:
            if gui != None:  gui.text.insert(END, '无法下载此视频\n')
            print("无法下载此视频")
            return
        video_url = text['data']['durl'][0]['url']
        print(video_url)

        # 判断当前目录下是否含有video文件夹
        if not os.path.exists(os.getcwd() + '\\video'):
            os.mkdir('video')

        video_path = os.getcwd() + '\\video\\{}.flv'.format(long_title)
        downVideo(video_url, url, video_path, gui=gui)
        print("视频下载完毕")
        print("下载视频位于" + video_path)
        if gui != None:  gui.text.insert(END, "视频下载完成\n")
        if gui != None:  gui.text.insert(END, "视频位于" + os.getcwd() + "\\video\n")

    else:
        if gui != None:  gui.text.insert(END, "网址输入错误\n")
        print("网址输入错误")



def main():
    GUI()

if __name__ == '__main__':
    main()
